package com.test.web.jdbc.execute;

import com.test.web.jdbc.JdbcConnection;
import com.test.web.jdbc.exception.JdbcException;
import com.test.web.jdbc.util.ColumnUtil;
import com.test.web.jdbc.util.SqlUtil;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;

/**
 * com.test.web.jdbc.execute
 *
 * @author 白雨浓
 * date: 2018/10/21 13:11
 * content: 执行 insert update delete
 */
public class ExecuteQuery {

    private String sql;
    private JdbcConnection jdbcConnection;

    public ExecuteQuery(String sql, JdbcConnection jdbcConnection) {
        this.sql = sql;
        this.jdbcConnection = jdbcConnection;
    }

    @SuppressWarnings("unchecked")
    public <T> T exec(Class<T> clz, Map<String, Object> paramMap) {

        if (paramMap == null) {
            return this.exec(clz, new ArrayList<>());
        }

        SqlUtil.Result result = SqlUtil.convert(this.sql, paramMap);
        this.sql = result.getSql();

        if ("java.util.Map".equals(clz.getTypeName())) {
            return (T) this.execReturnMap(result.getParamList());
        }
        return this.exec(clz, result.getParamList());
    }

    public <T> T exec(Class<T> clz, List<Object> paramList) {

        T target = this.newInstance(clz);
        Connection conn = jdbcConnection.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(sql)) {

            if (paramList != null) {
                for (int i = 0; i < paramList.size(); i++) {
                    ps.setObject(i + 1, paramList.get(i));
                }
            }

            try (ResultSet rs = ps.executeQuery()) {
                int columnCount = rs.getMetaData().getColumnCount();
                rs.next();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rs.getMetaData().getColumnName(i);
                    Object value = rs.getObject(i);
                    this.setValue(target, columnName, value);
                }
                if (rs.next()) {
                    throw new JdbcException("result not one");
                }

            }
        } catch (SQLException e) {
            throw new JdbcException("sql run fail: " + e.getMessage());
        }

        return target;
    }

    public Map<String, Object> execReturnMap(List<Object> paramList) {

        Map<String, Object> map = new HashMap<>();

        Connection conn = jdbcConnection.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(sql)) {

            if (paramList != null) {
                for (int i = 0; i < paramList.size(); i++) {
                    ps.setObject(i + 1, paramList.get(i));
                }
            }

            try (ResultSet rs = ps.executeQuery()) {
                int columnCount = rs.getMetaData().getColumnCount();
                rs.next();
                for (int i = 1; i <= columnCount; i++) {
                    String columnName = rs.getMetaData().getColumnName(i);
                    columnName = ColumnUtil.camelCaseName(columnName);
                    Object value = rs.getObject(i);
                    map.put(columnName, value);
                }
                if (rs.next()) {
                    throw new JdbcException("result not one");
                }

            }
        } catch (SQLException e) {
            throw new JdbcException("sql run fail: " + e.getMessage());
        }

        return map;
    }

    public long execCount(Map<String, Object> paramMap) {

        if (paramMap == null) {
            return this.execCount(new ArrayList<>());
        }

        SqlUtil.Result result = SqlUtil.convert(this.sql, paramMap);
        this.sql = result.getSql();

        return this.execCount(result.getParamList());
    }

    public long execCount(List<Object> paramList) {

        Connection conn = jdbcConnection.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(sql)) {

            if (paramList != null) {
                for (int i = 0; i < paramList.size(); i++) {
                    ps.setObject(i + 1, paramList.get(i));
                }
            }

            try (ResultSet rs = ps.executeQuery()) {
                rs.next();
                return rs.getLong(1);
            }
        } catch (SQLException e) {
            throw new JdbcException("sql run fail: " + e.getMessage());
        }
    }

    @SuppressWarnings("unchecked")
    public <T> List<T> execList(Class<T> clz, Map<String, Object> paramMap) {

        if (paramMap == null) {
            return this.execList(clz, new ArrayList<>());
        }

        SqlUtil.Result result = SqlUtil.convert(this.sql, paramMap);
        this.sql = result.getSql();

        if (clz == Map.class) {
            return (List<T>) this.execListReturnMap(result.getParamList());
        }

        return this.execList(clz, result.getParamList());
    }

    public <T> List<T> execList(Class<T> clz, List<Object> paramList) {

        List<T> targetList = new LinkedList<>();

        Connection conn = jdbcConnection.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(sql)) {

            if (paramList != null) {
                for (int i = 0; i < paramList.size(); i++) {
                    ps.setObject(i + 1, paramList.get(i));
                }
            }

            try (ResultSet rs = ps.executeQuery()) {
                int columnCount = rs.getMetaData().getColumnCount();
                while (rs.next()) {
                    T target = this.newInstance(clz);
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = rs.getMetaData().getColumnName(i);
                        Object value = rs.getObject(i);
                        this.setValue(target, columnName, value);
                    }
                    targetList.add(target);
                }
            }
        } catch (SQLException e) {
            throw new JdbcException("sql run fail: " + e.getMessage());
        }

        return targetList;
    }

    public List<Map<String, Object>> execListReturnMap(List<Object> paramList) {

        List<Map<String, Object>> targetList = new LinkedList<>();

        Connection conn = jdbcConnection.getConnection();
        try (PreparedStatement ps = conn.prepareStatement(sql)) {

            if (paramList != null) {
                for (int i = 0; i < paramList.size(); i++) {
                    ps.setObject(i + 1, paramList.get(i));
                }
            }

            try (ResultSet rs = ps.executeQuery()) {
                int columnCount = rs.getMetaData().getColumnCount();
                while (rs.next()) {
                    Map<String, Object> target = new HashMap<>();
                    for (int i = 1; i <= columnCount; i++) {
                        String columnName = rs.getMetaData().getColumnName(i);
                        columnName = ColumnUtil.camelCaseName(columnName);
                        Object value = rs.getObject(i);
                        target.put(columnName, value);
                    }
                    targetList.add(target);
                }
            }
        } catch (SQLException e) {
            throw new JdbcException("sql run fail: " + e.getMessage());
        }

        return targetList;
    }

    private <T> T newInstance(Class<T> clz) {
        T target;
        try {
            target = clz.getDeclaredConstructor().newInstance();
        } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
            throw new JdbcException(String.format("init %s fail : %s",
                    clz.getName(), e.getMessage()));
        }
        return target;
    }

    private <T> void setValue(T target, String name, Object value) {

        String fieldName = ColumnUtil.camelCaseName(name);
        Method method = this.getSetMethod(target, fieldName);

        if (method == null) {
            return;
        }

        try {
            method.invoke(target, value);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new JdbcException(String.format("%s() fail: %s", method.getName(), e.getMessage()));
        }
    }

    private <T> Method getSetMethod(T target, String fieldName) {
        Class fieldType;
        try {
            fieldType = target.getClass().getDeclaredField(fieldName).getType();
        } catch (NoSuchFieldException e) {
            return null;
        }

        try {
            String methodName = "set" + ColumnUtil.upperCaseFirstOne(fieldName);
            return target.getClass().getMethod(methodName, fieldType);
        } catch (NoSuchMethodException e) {
            return null;
        }
    }

}
